package pkg

import (
	"fmt"
	"slices"
	"sort"
	"strings"
	"time"

	trivydbTypes "github.com/aquasecurity/trivy-db/pkg/types"
	ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/types"

	"github.com/future-architect/vuls/models"
)

// Convert :
func Convert(results types.Results) (result *models.ScanResult, err error) {
	scanResult := &models.ScanResult{
		JSONVersion: models.JSONVersion,
		ScannedCves: models.VulnInfos{},
	}

	pkgs := models.Packages{}
	srcPkgs := models.SrcPackages{}
	vulnInfos := models.VulnInfos{}
	uniqueLibraryScannerPaths := map[string]models.LibraryScanner{}
	for _, trivyResult := range results {
		for _, vuln := range trivyResult.Vulnerabilities {
			if _, ok := vulnInfos[vuln.VulnerabilityID]; !ok {
				vulnInfos[vuln.VulnerabilityID] = models.VulnInfo{
					CveID: vuln.VulnerabilityID,
					Confidences: models.Confidences{
						{
							Score:           100,
							DetectionMethod: models.TrivyMatchStr,
						},
					},
					AffectedPackages: models.PackageFixStatuses{},
					CveContents:      models.CveContents{},
					LibraryFixedIns:  models.LibraryFixedIns{},
					// VulnType : "",
				}
			}
			vulnInfo := vulnInfos[vuln.VulnerabilityID]
			var notFixedYet bool
			fixState := ""
			if len(vuln.FixedVersion) == 0 {
				notFixedYet = true
				fixState = "Affected"
			}
			var references models.References
			for _, reference := range vuln.References {
				references = append(references, models.Reference{
					Source: "trivy",
					Link:   reference,
				})
			}

			sort.Slice(references, func(i, j int) bool {
				return references[i].Link < references[j].Link
			})

			var published time.Time
			if vuln.PublishedDate != nil {
				published = *vuln.PublishedDate
			}

			var lastModified time.Time
			if vuln.LastModifiedDate != nil {
				lastModified = *vuln.LastModifiedDate
			}

			for source, severity := range vuln.VendorSeverity {
				severities := []string{trivydbTypes.SeverityNames[severity]}
				if cs, ok := vulnInfo.CveContents[models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source))]; ok {
					for _, c := range cs {
						for _, s := range strings.Split(c.Cvss3Severity, "|") {
							if s != "" && !slices.Contains(severities, s) {
								severities = append(severities, s)
							}
						}
					}
				}
				slices.SortFunc(severities, func(a, b string) int { return -trivydbTypes.CompareSeverityString(a, b) })

				vulnInfo.CveContents[models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source))] = []models.CveContent{{
					Type:          models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source)),
					CveID:         vuln.VulnerabilityID,
					Title:         vuln.Title,
					Summary:       vuln.Description,
					Cvss3Severity: strings.Join(severities, "|"),
					Published:     published,
					LastModified:  lastModified,
					References:    references,
				}}
			}

			for source, cvss := range vuln.CVSS {
				if cs, ok := vulnInfo.CveContents[models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source))]; ok &&
					slices.ContainsFunc(cs, func(c models.CveContent) bool {
						return c.Cvss2Score == cvss.V2Score && c.Cvss2Vector == cvss.V2Vector && c.Cvss3Score == cvss.V3Score && c.Cvss3Vector == cvss.V3Vector && c.Cvss40Score == cvss.V40Score && c.Cvss40Vector == cvss.V40Vector
					}) {
					continue
				}

				vulnInfo.CveContents[models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source))] = append(vulnInfo.CveContents[models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source))], models.CveContent{
					Type:         models.CveContentType(fmt.Sprintf("%s:%s", models.Trivy, source)),
					CveID:        vuln.VulnerabilityID,
					Title:        vuln.Title,
					Summary:      vuln.Description,
					Cvss2Score:   cvss.V2Score,
					Cvss2Vector:  cvss.V2Vector,
					Cvss3Score:   cvss.V3Score,
					Cvss3Vector:  cvss.V3Vector,
					Cvss40Score:  cvss.V40Score,
					Cvss40Vector: cvss.V40Vector,
					Published:    published,
					LastModified: lastModified,
					References:   references,
				})
			}

			// do only if image type is Vuln
			if isTrivySupportedOS(trivyResult.Type) {
				pkgs[vuln.PkgName] = models.Package{
					Name:    vuln.PkgName,
					Version: vuln.InstalledVersion,
				}
				vulnInfo.AffectedPackages = append(vulnInfo.AffectedPackages, models.PackageFixStatus{
					Name:        vuln.PkgName,
					NotFixedYet: notFixedYet,
					FixState:    fixState,
					FixedIn:     vuln.FixedVersion,
				})
			} else {
				vulnInfo.LibraryFixedIns = append(vulnInfo.LibraryFixedIns, models.LibraryFixedIn{
					Key:     string(trivyResult.Type),
					Name:    vuln.PkgName,
					Version: vuln.InstalledVersion,
					Path:    trivyResult.Target,
					FixedIn: vuln.FixedVersion,
				})
				libScanner := uniqueLibraryScannerPaths[trivyResult.Target]
				libScanner.Type = trivyResult.Type
				libScanner.Libs = append(libScanner.Libs, models.Library{
					Name:     vuln.PkgName,
					Version:  vuln.InstalledVersion,
					FilePath: vuln.PkgPath,
				})
				uniqueLibraryScannerPaths[trivyResult.Target] = libScanner
			}
			vulnInfos[vuln.VulnerabilityID] = vulnInfo
		}

		// --list-all-pkgs flg of trivy will output all installed packages, so collect them.
		switch trivyResult.Class {
		case types.ClassOSPkg:
			for _, p := range trivyResult.Packages {
				pv := p.Version
				if p.Release != "" {
					pv = fmt.Sprintf("%s-%s", pv, p.Release)
				}
				if p.Epoch > 0 {
					pv = fmt.Sprintf("%d:%s", p.Epoch, pv)
				}
				pkgs[p.Name] = models.Package{
					Name:    p.Name,
					Version: pv,
					Arch:    p.Arch,
				}

				v, ok := srcPkgs[p.SrcName]
				if !ok {
					sv := p.SrcVersion
					if p.SrcRelease != "" {
						sv = fmt.Sprintf("%s-%s", sv, p.SrcRelease)
					}
					if p.SrcEpoch > 0 {
						sv = fmt.Sprintf("%d:%s", p.SrcEpoch, sv)
					}
					v = models.SrcPackage{
						Name:    p.SrcName,
						Version: sv,
					}
				}
				v.AddBinaryName(p.Name)
				srcPkgs[p.SrcName] = v
			}
		case types.ClassLangPkg:
			libScanner := uniqueLibraryScannerPaths[trivyResult.Target]
			libScanner.Type = trivyResult.Type
			for _, p := range trivyResult.Packages {
				libScanner.Libs = append(libScanner.Libs, models.Library{
					Name:     p.Name,
					Version:  p.Version,
					PURL:     getPURL(p),
					FilePath: p.FilePath,
				})
			}
			uniqueLibraryScannerPaths[trivyResult.Target] = libScanner
		default:
		}
	}

	// flatten and unique libraries
	libraryScanners := make([]models.LibraryScanner, 0, len(uniqueLibraryScannerPaths))
	for path, v := range uniqueLibraryScannerPaths {
		uniqueLibrary := map[string]models.Library{}
		for _, lib := range v.Libs {
			uniqueLibrary[lib.Name+lib.Version] = lib
		}

		var libraries []models.Library
		for _, library := range uniqueLibrary {
			libraries = append(libraries, library)
		}

		sort.Slice(libraries, func(i, j int) bool {
			return libraries[i].Name < libraries[j].Name
		})

		libscanner := models.LibraryScanner{
			Type:         v.Type,
			LockfilePath: path,
			Libs:         libraries,
		}
		libraryScanners = append(libraryScanners, libscanner)
	}
	sort.Slice(libraryScanners, func(i, j int) bool {
		return libraryScanners[i].LockfilePath < libraryScanners[j].LockfilePath
	})
	scanResult.ScannedCves = vulnInfos
	scanResult.Packages = pkgs
	scanResult.SrcPackages = srcPkgs
	scanResult.LibraryScanners = libraryScanners
	return scanResult, nil
}

func isTrivySupportedOS(family ftypes.TargetType) bool {
	supportedFamilies := map[ftypes.TargetType]struct{}{
		ftypes.Alma:               {},
		ftypes.Alpine:             {},
		ftypes.Amazon:             {},
		ftypes.Azure:              {},
		ftypes.Bottlerocket:       {},
		ftypes.CBLMariner:         {},
		ftypes.CentOS:             {},
		ftypes.Chainguard:         {},
		ftypes.CoreOS:             {},
		ftypes.Debian:             {},
		ftypes.Echo:               {},
		ftypes.Fedora:             {},
		ftypes.MinimOS:            {},
		ftypes.OpenSUSE:           {},
		ftypes.OpenSUSELeap:       {},
		ftypes.OpenSUSETumbleweed: {},
		ftypes.Oracle:             {},
		ftypes.Photon:             {},
		ftypes.RedHat:             {},
		ftypes.Rocky:              {},
		ftypes.SLEMicro:           {},
		ftypes.SLES:               {},
		ftypes.Ubuntu:             {},
		ftypes.Wolfi:              {},
	}
	_, ok := supportedFamilies[family]
	return ok
}

func getPURL(p ftypes.Package) string {
	if p.Identifier.PURL == nil {
		return ""
	}
	return p.Identifier.PURL.String()
}
